<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">
    <title>简单拖动布局</title>
    <style type="text/css">
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        .main {
            display: flex;
            width: 100vw;
            height: 80vh;
            border: 1px solid #aaaaaa;
        }

        .utils {
            width: 12vw;
            height: 100%;

            .item {
                margin: 8px 0;
                padding: 8px;
                background-color: aliceblue;
            }

            .item.moving {
                background-color: #e49077;
                color: #fff;
            }
        }

        .preview {
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
            flex: 1;
            height: 100%;
            border-left: 1px solid #aaaaaa;
            border-right: 1px solid #aaaaaa;
        }

        .mobile-wrap {
            position: relative;
            margin-top: 20px;
            height: 80%;
            border: 4px solid #080707;
            border-radius: 10px;
            overflow: hidden;
        }

        .mobile {
            position: relative;
            width: 375px;
            height: 100%;
        }

        .mobile .node-item {
            position: absolute;
            left: 0;
            top: 0;
            cursor: move;
        }

        .editor {
            width: 24vw;
            height: 100%;
        }

        .editor h4 {
            padding: 8px 0;
            text-align: center;
        }

        .editor .editor-item {
            display: flex;
            padding: 8px 4px;
        }

        .editor .decr,
        .editor .add {
            padding: 4px 8px;
            border-radius: 4px;
            background-color: rgb(64, 190, 244);
            cursor: pointer;
            outline: none;
        }

        .editor .decr {
            margin-left: 8px;
            color: #fff;
        }

        .editor .add {
            margin-left: 8px;
            color: #fff;
        }

        .json {
            display: block;
            padding: 8px 20px;
            border-radius: 8px;
            margin: 0 auto;
            margin-top: 200px;
            background-color: rgb(128, 128, 239);
            color: #fff;
            cursor: pointer;
        }

        .active {
            /* animation: ani 1s linear infinite; */
        }

        @keyframes ani {
            from {
                background-color: #fff;
            }

            to {
                background-color: rgb(252, 141, 141);
            }
        }

        .editor-item {
            input {
                width: 80px;
                border: 1px solid #ccc;
                border-radius: 4px;
            }
        }

        .style-wrap {
            width: 100%;
            padding: 4px;
            border-radius: 4px;
            box-shadow: -3px 5px 9px 4px rgba(0, 0, 0, .1);

            .title {
                padding: 4px 0;
                text-align: center;
                font-weight: bold;
            }
        }

        .utils-box-shadow-wrap {
            width: 100%;
            padding: 4px;
            border-radius: 4px;
            box-shadow: -3px 5px 9px 4px rgba(0, 0, 0, .1);

            .title {
                text-align: center;
                font-weight: bold;
            }

            .box-shadow {
                .util-item {
                    display: flex;
                    padding: 4px 0;

                    .key {
                        width: 60px;
                    }

                    .val {
                        flex: 1;

                        .type {
                            display: flex;

                            .btn {
                                margin: 0 4px;
                            }
                        }
                    }
                }
            }
        }


        .utils-text-align {
            width: 100%;
            padding: 4px;
            border-radius: 4px;
            box-shadow: -3px 5px 9px 4px rgba(0, 0, 0, .1);

            .title {
                text-align: center;
                font-weight: bold;
            }

            .radio-wrap {
                display: flex;
                justify-content: space-around;

                input {
                    margin-left: 4px;
                }
            }
        }

        .utils-border-radius {
            width: 100%;
            padding: 4px;
            border-radius: 4px;
            box-shadow: -3px 5px 9px 4px rgba(0, 0, 0, .1);

            .title {
                text-align: center;
                font-weight: bold;
            }

            .radio-wrap {
                display: flex;
                flex-direction: column;
                justify-content: space-around;

                input {
                    margin-left: 4px;
                }
            }
        }
    </style>
</head>

<body>
    <div class="main" id="app">
        <div class="utils">
            <div class="list" @dragstart="ondragstart" :class="dragObj.className">
                <div draggable="true" class="item" v-for="item in materialList" :data-type="item.nodeName">{{item.desc}}
                </div>
            </div>
        </div>
        <div class="preview">
            <div class="mobile-wrap">
                <div class="mobile" @dragover="ondragover" @drop="ondrop">
                    <component :is="item.el" @dragstart="(e)=>{e.preventDefault();return false}" :src="item.src"
                        v-for="(item,index) in renderList" :key="item.className" :data-index="index"
                        :style="item.renderStyle" :class="item.className">
                        {{item.content}}
                    </component>
                </div>
            </div>
        </div>
        <div class="editor" v-if="showUtils">
            <h4>{{showUtils }}属性</h4>
            <h5>定位属性</h5>
            <div class="editor-item" v-for="(item,key) in getRectAttribute">{{item.name}}:
                <input type="number" v-model="item.value" @input="inputChange($event,item, 'rect')">
                <button class="decr" @click="decr(item.attr,item.value,'rect')">-</button>
                <button class="add" @click="add(item.attr,item.value,'rect')">+</button>
            </div>
            <h5>样式属性</h5>
            <div class="editor-item" v-for="(item,key) in getStyleAttribute">
                <div class="style-wrap" v-if="['text','color','number'].includes(item.widget)">
                    <div class="title">{{item.name}}</div>
                    <input :type="item.widget" v-model="item.value" @input="inputChange($event,item, 'style')">
                </div>
                <div class="utils-box-shadow-wrap" v-else-if="item.widget === 'box-shadow'">
                    <div class="title">阴影</div>
                    <div class="box-shadow">
                        <div class="util-item">
                            <div class="key">类型: </div>
                            <div class="val">
                                <div class="type">
                                    <div class="btn">outset</div>
                                    <div class="btn">inset</div>
                                </div>
                            </div>
                        </div>
                        <div class="util-item">
                            <div class="key">x偏移: </div>
                            <div class="val">
                                <input type="number">
                            </div>
                        </div>
                        <div class="util-item">
                            <div class="key">y偏移: </div>
                            <div class="val">
                                <input type="number">
                            </div>
                        </div>
                        <div class="util-item">
                            <div class="key">模糊: </div>
                            <div class="val">
                                <input type="number">
                                <input type="range">
                            </div>
                        </div>
                        <div class="util-item">
                            <div class="key">扩张: </div>
                            <div class="val">
                                <input type="number">
                                <input type="range">
                            </div>
                        </div>
                    </div>
                </div>
                <div class="utils-text-align" v-else-if="item.widget === 'text-align'">
                    <div class="title">文本对齐</div>
                    <div class="radio-wrap">
                        <div class="radio" v-for="align in ['left','center','right']">
                            <input type="radio" v-model="textAlignSty" :value="align">
                            <label :for="align">{{align}}</label>
                        </div>
                    </div>
                </div>
                <div class="utils-border-radius" v-else-if="item.widget === 'border-radius'">
                    <div class="title">圆角</div>
                    <div class="radio-wrap">
                        <div class="radio"
                            v-for="dir in [{name:'左上角',value:''},{name:'右上角',value:''},{name:'右下角',value:''},{name:'左下角',value:''}]">
                            <label :for="dir.name">{{dir.name}}</label>
                            <input type="number" v-model="dir.value">
                        </div>
                    </div>
                </div>
            </div>

            <div class="editor-item" v-if="getSrc">图片链接: <input type="text" v-model="getSrc"></div>
            <div class="editor-item" v-if="getContent">文本: <input type="text" v-model="getContent"></div>
            <button class="json" type="button" @click="getJson">导出json</button>
        </div>
    </div>
    <script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/3.2.31/vue.global.min.js"></script>
    <script src="./schema.js"></script>
    <script>
        let wrapOffset = {}
        function parseStyle(v) {
            const num = parseFloat(v)
            return Number.isNaN(num) ? v : num
        }
        function getNumAndStr(val) {
            val += ''
            // 解析出val字符串中非数字的部分
            const str = val.replace(/[^a-zA-Z%]/g, '') || 'px'
            // 获取val字符串中数字部分
            const num = val.replace(/[^0-9]/g, '') || '0'
            // 拼接出新的字符串
            return { num: parseInt(num), str }
        }
        function copyObj(params) {
            return JSON.parse(JSON.stringify(params))
        }
        Vue.createApp({
            data() {
                return {
                    textAlignSty: '',
                    radiusSty: '',

                    designWith: 750,
                    curDragNodeIndex: 0,

                    dragObj: {},
                    renderList: [],
                    materialList: [],
                }
            },
            computed: {
                showUtils() {
                    return this.renderList[this.curDragNodeIndex] && this.renderList[this.curDragNodeIndex].desc
                },
                getCurentNodeRenderStyle: {
                    get() {
                        return this.renderList[this.curDragNodeIndex]?.renderStyle
                    },
                    set(val) {
                        this.renderList[this.curDragNodeIndex].content = val
                    }
                },
                getContent: {
                    get() {
                        return this.renderList[this.curDragNodeIndex]?.content
                    },
                    set(val) {
                        this.renderList[this.curDragNodeIndex].content = val
                    }
                },
                getSrc: {
                    get() {
                        return this.renderList[this.curDragNodeIndex]?.src
                    },
                    set(val) {
                        this.renderList[this.curDragNodeIndex].src = val
                    }
                },
                getStyleAttribute() {
                    return this.renderList[this.curDragNodeIndex]?.material?.style
                },
                getRectAttribute() {
                    return this.renderList[this.curDragNodeIndex]?.material?.rect || []
                }
            },
            methods: {
                getJson() {
                    // 包含基底则以baseView为根节点，否则以mobilew为跟节点计算偏移
                    const baseView = this.renderList?.find(e => e.nodeName === 'baseView')
                    if (baseView) {
                        const baseViewRect = baseView.renderStyle
                        const normalizedRect = copyObj(this.renderList).map(e => {
                            const { renderStyle, ...rest } = e
                            return {
                                ...rest,
                                renderStyle: {
                                    ...renderStyle,
                                    left: parseFloat(renderStyle.left) - parseFloat(baseViewRect.left),
                                    top: parseFloat(renderStyle.top) - parseFloat(baseViewRect.top)
                                }
                            }
                        })
                        console.log(normalizedRect)
                    } else {
                        console.log(this.renderList)
                    }
                },
                add(attr, val) {
                    val++
                    this.updateNodeRectAttr({ [attr]: Math.min(Math.max(0, val), wrapOffset.height) })
                },
                decr(attr, val) {
                    val--
                    this.updateNodeRectAttr({ [attr]: Math.min(Math.max(0, val), wrapOffset.height) })
                },
                isNumType(v) {
                    const num = parseFloat(v)
                    return !Number.isNaN(num)
                },
                inputChange(e, v, type) {
                    console.log(e.target.value);

                    console.log(v, type)
                    if (type === 'rect') {
                        this.updateNodeRectAttr({ [v.attr]: v.value })
                    } else {
                        this.updateNodeStyleAttr({ [v.attr]: e.target.value })
                    }
                },
                ondragstart(e) {
                    this.dragObj = {
                        className: 'moving',
                        type: e.target.getAttribute('data-type')
                    }
                },
                ondragover(e) {
                    e.preventDefault()
                },
                ondrop(e) {
                    e.preventDefault();

                    if (!this.dragObj.type) return
                    const elType = this.dragObj.type

                    const config = pageSchema[elType]
                    if (!config) return

                    const rawRect = config.rect
                    const normalizedRect = {}
                    const rectMaterial = {}

                    for (const key in rawRect) {
                        if (typeof rawRect[key] === 'number') {
                            normalizedRect[key] = rawRect[key] + 'px'
                        } else if (typeof rawRect[key] === 'string') {
                            if (rawRect[key].endsWith('%')) { // 将百分比转为px
                                normalizedRect[key] = (wrapOffset[key] / 100) * parseFloat(rawRect[key]) + 'px'
                            } else if (rawRect[key].endsWith('px')) { // px
                                normalizedRect[key] = rawRect[key]
                            } else {
                                normalizedRect[key] = rawRect[key]
                            }
                        }
                        if (key === 'width') {
                            rectMaterial.width = {
                                name: '宽度',
                                attr: 'width',
                                type: 'number',
                                value: parseFloat(normalizedRect[key])
                            }
                        } else if (key === 'height') {
                            rectMaterial.height = {
                                name: '高度',
                                attr: 'height',
                                type: 'number',
                                value: parseFloat(normalizedRect[key])
                            }
                        } else if (key === 'top') {
                            rectMaterial.top = {
                                name: '上边距',
                                attr: 'top',
                                type: 'number',
                                value: parseFloat(normalizedRect[key])
                            }
                        } else {
                            rectMaterial.left = {
                                name: '左边距',
                                attr: 'left',
                                type: 'number',
                                value: parseFloat(normalizedRect[key])
                            }
                        }
                    }
                    const renderStyle = Object.assign({}, normalizedRect, config.style, {
                        left: 0,
                        top: 0,
                    })
                    const materialStyle = config.material.map(e => {
                        return {
                            ...e,
                            value: renderStyle[e.attr]
                        }
                    })
                    const nodeConfig = {
                        desc: config.desc,
                        el: config.el,
                        className: 'node-item',
                        renderStyle,
                        material: { rect: rectMaterial, style: materialStyle },
                    }
                    this.renderList[this.curDragNodeIndex] && (this.renderList[this.curDragNodeIndex].className = 'node-item')
                    this.curDragNodeIndex = this.renderList.length
                    this.renderList.push(nodeConfig)

                    this.dragObj.className = ''
                },
                getPhoneWrapRect() {
                    const el = document.querySelector('.mobile')
                    return el.getBoundingClientRect()
                },
                updateNodeRectAttr(attrs) {
                    const cur = this.renderList[this.curDragNodeIndex]
                    if (cur) {
                        for (const key in attrs) {
                            cur.renderStyle[key] = attrs[key] + 'px'
                            cur.material.rect[key].value = attrs[key]
                        }
                    }
                },
                updateNodeStyleAttr(attrs) {
                    console.log(attrs);

                    const cur = this.renderList[this.curDragNodeIndex]
                    if (cur) {
                        for (const key in attrs) {
                            cur.renderStyle[key] = attrs[key]
                        }
                    }
                }
            },
            mounted() {
                wrapOffset = this.getPhoneWrapRect()

                const onmousedown = (event) => {
                    const drag = event.target

                    event = event || window.event;  //兼容IE浏览器
                    //    鼠标点击物体那一刻相对于物体左侧边框的距离=点击时的位置相对于浏览器最左边的距离-物体左边框相对于浏览器最左边的距离
                    const diffX = event.clientX - drag.offsetLeft;
                    const diffY = event.clientY - drag.offsetTop;
                    if (typeof drag.setCapture !== 'undefined') {
                        drag.setCapture();
                    }
                    if (drag.className.includes('node-item')) {
                        this.renderList[this.curDragNodeIndex] && (this.renderList[this.curDragNodeIndex].className = 'node-item')
                        this.curDragNodeIndex = Number(drag.getAttribute('data-index'))
                        this.renderList[this.curDragNodeIndex].className = 'active node-item'
                    }
                    console.log(wrapOffset.width, drag.offsetWidth);

                    document.onmousemove = (event) => {
                        event = event || window.event;
                        let moveX = event.clientX - diffX;
                        let moveY = event.clientY - diffY;
                        if (moveX < 0) {
                            moveX = 0
                        } else if (moveX > wrapOffset.width - drag.offsetWidth) {
                            moveX = wrapOffset.width - drag.offsetWidth
                        }
                        if (moveY < 0) {
                            moveY = 0
                        } else if (moveY > wrapOffset.height - drag.offsetHeight) {
                            moveY = wrapOffset.height - drag.offsetHeight
                        }
                        this.updateNodeRectAttr({ left: moveX, top: moveY })
                        event.preventDefault();
                    }
                    document.onmouseup = function(e) {
                        e.preventDefault();
                        document.onmousemove = null;
                        document.onmouseup = null;
                        //修复低版本ie bug  
                        if (typeof drag.releaseCapture != 'undefined') {
                            drag.releaseCapture();
                        }
                    }
                }
                document.addEventListener('mousedown', (e) => {
                    if (e.target.className.includes('node-item')) {
                        onmousedown(e)
                    }
                })
                this.materialList = Object.values(pageSchema).map(e => ({ nodeName: e.nodeName, desc: e.desc }))
            }
        }).mount('#app')
    </script>
</body>

</html>